package org.aceor.mddal.group.selector;

import org.aceor.mddal.group.DataSourceWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 按优先级选择的selector
 * <p>
 * 每次选择只从优先级最高的一组DB中选择，若都不可用，才继续在下一个优先级的DB组中选择
 * <p>
 * 优先级相同的DB还用随机选择
 * <p>
 * 原始需求：TC要求在每个dbgroup中优先读备库，当备库不可用时，自动读主库
 * 扩展需求：一主多备，优先随机读备库。当备库都不可用时，才读主库
 * <p>
 * 为了方便处理和接口一致，有如下要求：
 * 1. 目前只支持读分优先级组.即按p分组.没有实现按q的写分组
 * 2. 一个权重推送的信息中，。。。
 * 3. 一个数据源只能在一个优先级组中？
 *
 * @author linxuan
 */
public class PriorityDBGroupSelector extends AbstractDBSelector {
    private static final Logger logger = LoggerFactory.getLogger(PriorityDBGroupSelector.class);

    /**
     * 按优先级顺序存放数据库组。元素0优先级最高。每个EquityDbManager元素代表具有相同优先级的一组数据库
     */
    private EquityDBSelector[] priorityGroups;

    public PriorityDBGroupSelector(EquityDBSelector[] priorityGroups) {
        this.priorityGroups = priorityGroups;
        if (priorityGroups == null || priorityGroups.length == 0) {
            throw new IllegalArgumentException("EquityDBSelector[] priorityGroups is null or empty");
        }
    }

    public DataSource select() {
        for (int i = 0; i < priorityGroups.length; i++) {
            DataSource ds = priorityGroups[i].select();
            if (ds != null) {
                return ds;
            }
        }
        return null;
    }

    public DataSourceWrapper get(String dsKey) {
        for (int i = 0; i < priorityGroups.length; i++) {
            DataSourceWrapper ds = priorityGroups[i].get(dsKey);
            if (ds != null) {
                return ds;
            }
        }
        return null;
    }

    private static class DataSourceTryerWrapper<T> implements DataSourceTryer<T> {
        private final List<SQLException> historyExceptions;
        private final DataSourceTryer<T> tryer;

        public DataSourceTryerWrapper(DataSourceTryer<T> tryer, List<SQLException> historyExceptions) {
            this.tryer = tryer;
            this.historyExceptions = historyExceptions;
        }

        public T onSQLException(List<SQLException> exceptions, Object... args)
                throws SQLException {
            Exception last = exceptions.get(exceptions.size() - 1);
            //这里很机智!
            if (last instanceof NoMoreDataSourceException) {
                if (exceptions.size() > 1) {
                    exceptions.remove(exceptions.size() - 1);
                }
                historyExceptions.addAll(exceptions);
                throw (NoMoreDataSourceException) last;
            } else {
                return tryer.onSQLException(exceptions, args);
            }
        }

        public T tryOnDataSource(DataSourceWrapper dsw, Object... args) throws SQLException {
            return tryer.tryOnDataSource(dsw, args);
        }
    }

    /**
     * 基于EquityDbManager的tryExecute实现，对用户的tryer做一个包装，在wrapperTryer.onSQLException中
     * 检测到最后一个e是NoMoreDataSourceException时，不调原tryer的onSQLException, 转而重试其他优先级的
     */
    protected <T> T tryExecuteInternal(Map<DataSource, SQLException> failedDataSources, DataSourceTryer<T> tryer,
                                       int times, Object... args) throws SQLException {
        final List<SQLException> historyExceptions = new ArrayList<SQLException>(0);
        DataSourceTryer<T> wrapperTryer = new DataSourceTryerWrapper<T>(tryer, historyExceptions); //移花接木

        for (int i = 0; i < priorityGroups.length; i++) {
            try {
                return priorityGroups[i].tryExecute(failedDataSources, wrapperTryer, times, args);
            } catch (NoMoreDataSourceException e) {
                //catch到前一个EquityDbSelector没找到对应数据源.ignore异常,继续下一个优先级的EquityDbSelector查找.
                //这里虽然ignore了异常,但是每个循环,都把异常放到了historyExceptions中了
                logger.warn("NoMoreDataSource for retry for priority group " + i);
            }
        }
        //所有的优先级组都不可用，则抛出异常
        return tryer.onSQLException(historyExceptions, args);
    }

    @Override
    public void setSupportRetry(boolean isSupportRetry) {
        for (int i = 0; i < priorityGroups.length; i++) {
            priorityGroups[i].setSupportRetry(isSupportRetry);
        }
        this.isSupportRetry = isSupportRetry;
    }

    public void setReadable(boolean readable) {
        for (int i = 0; i < priorityGroups.length; i++) {
            priorityGroups[i].setReadable(readable);
        }

        this.readable = readable;
    }
}
